﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.IO;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using SharePoint.Contrib.Extensions;

namespace SharePoint.Contrib
{
    /// <summary>
    /// Utility class that handles updates to web.config inside SharePoint
    /// </summary>
    public class WebConfigManager : IDisposable
    {
        #region [Members]

        private XDocument webconfig;
        private uint sequence = 0;
        private SPWebApplication application;
        private string owner;
        private StringWriter log;
        private bool attributemode = false; 

        #endregion

        #region [Events]

        public event EventHandler<EventArgs<string>> Logging = delegate { }; 

        #endregion

        #region [Methods]

        private WebConfigManager()
        {
            this.log = new StringWriter();
        }

        private WebConfigManager(XDocument webconfig, SPWebApplication application)
            : this()
        {
            this.webconfig = webconfig;
            this.application = application;
        }

        public WebConfigManager(XDocument webconfig, SPWebApplication application, string owner)
            : this(webconfig, application)
        {
            this.owner = owner;
        }

        public WebConfigManager(XDocument webconfig, SPWebApplication application, SPFeatureReceiverProperties properties)
            : this(webconfig, application)
        {
            this.owner = properties.Feature.DefinitionId.ToString();
        }

        public WebConfigManager(SPFeatureReceiverProperties properties)
            : this()
        {
            this.owner = properties.Feature.DefinitionId.ToString();
            this.application = properties.Feature.Parent as SPWebApplication;

            if (this.application == null)
            {
                log.WriteLine("Trying to extract the webapplication from SPFeatureReceiverProperties");
                ExtractWebApplicationFrom(properties);
            }

            ReadWebConfigFromFeatureDirectory(properties);
        }

        /// <summary>
        /// Removes all the modifications in the WebConfigModifications collection
        /// </summary>
        /// <remarks>Beware: this deletes all of them. Remember to call Commit()</remarks>
        public void RemoveAllModifications()
        {
            log.WriteLine("Removing all modifications");

            foreach (var modification in application.WebConfigModifications.ToArray())
            {
                try
                {
                    log.WriteLine("Removing modification named '{0}'", modification.Name);
                    application.WebConfigModifications.Remove(modification);
                }
                catch (Exception e)
                {
                    log.WriteLine("# An error occured when removing modification '{0}' {2}# {1}", modification.Name, e, Environment.NewLine);
                }
            }
        }

        /// <summary>
        /// Removes all modifications that have been made by the owner that created this instance of the <see cref="WebConfigManager"/>.
        /// </summary>
        /// <remarks>Don´t forget to call the Commit method to write the changes into SharePoint.</remarks>
        public void RemoveModifications()
        {
            log.WriteLine("Removing all modifications made by owner '{0}'", this.owner);
            Remove(Modification => Modification.Owner == owner);
        }

        /// <summary>
        /// Removes all modifications that matches the Func delegate.
        /// </summary>
        /// <remarks>Don´t forget to call the Commit method to write the changes into SharePoint.</remarks>
        public void Remove(Func<SPWebConfigModification, bool> match)
        {

            // filter out all results that are not sections (that is to buggy)
            var result = application.WebConfigModifications
                            .Where(Config => Config.Type != SPWebConfigModification.SPWebConfigModificationType.EnsureSection)
                            .Where(match).ToArray();


            foreach (var modification in result)
            {
                try
                {
                    log.WriteLine("Removing modification named '{0}'", modification.Name);
                    application.WebConfigModifications.Remove(modification);
                }
                catch (Exception e)
                {
                    log.WriteLine("# An error occured when removing modification '{0}' {2}# {1}", modification.Name, e, Environment.NewLine);
                }
            }

        }

        /// <summary>
        /// Applies all the changes that are found in the web.config file supplied to the <see cref="WebConfigManager"/>.
        /// </summary>
        /// <remarks>Don´t forget to call the Commit method to write the changes into SharePoint.
        /// <para>Will run with attributesOnly set to false.</para></remarks>
        public void ApplyChanges()
        {
            ApplyChanges(false);
        }

        /// <summary>
        /// Applies all the changes that are found in the web.config file supplied to the <see cref="WebConfigManager"/>.
        /// </summary>
        /// <remarks>Don´t forget to call the Commit method to write the changes into SharePoint.</remarks>
        /// <param name="attributesOnly">Indicates whether the web.config file only has changes to attributes. This is special switch</param>
        public void ApplyChanges(bool attributesOnly)
        {
            log.WriteLine("Applying all changes found in feature web.config file");

            HandleElementsWithNamespaces();


            if (attributesOnly)
            {
                attributemode = true;

                // Handle all attributes
                AddModifications(from node in FindConfigurationNodes()
                                 where node.HasAttributes
                                 select node,
                                SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute);
            }
            else
            {
                // Handle all sections
                AddModifications(from node in FindConfigurationNodes()
                                 where node.HasAttributes == false
                                 select node,
                                SPWebConfigModification.SPWebConfigModificationType.EnsureSection);

                // Handle all childnodes
                AddModifications(from node in FindConfigurationNodes()
                                 where node.HasAttributes
                                 select node,
                                SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            }

        }

        /// <summary>
        /// Finds all modifications that have been made by the owner that created this instance of the <see cref="WebConfigManager"/>.
        /// </summary>
        public string FindAllModications()
        {
            log.WriteLine("Trying to find all modifications made by owner '{0}'", this.owner);
            return FindAllModications(Modification => Modification.Owner == owner);
        }

        /// <summary>
        /// Finds all modifications that matches the Func delegate.
        /// </summary>
        public string FindAllModications(Func<SPWebConfigModification, bool> match)
        {

            var modifications = application.WebConfigModifications
                                    .Where(match).ToArray();

            StringBuilder sb = new StringBuilder(modifications.Length);

            foreach (var modification in modifications)
            {
                sb.AppendFormat("Name: {0}{5}XPath: {1} {5}Value: {2}{5}Type:{3}{5}Owner:{4}{5}{5}",
                    modification.Name,
                    modification.Path,
                    modification.Value,
                    modification.Type,
                    modification.Owner,
                    Environment.NewLine);
            }

            return sb.ToString();

        }

        /// <summary>
        /// Commits all the changes to SharePoint farm
        /// </summary>
        public void Commit()
        {
            try
            {
                application.Farm.Services.GetValue<SPWebService>(application.Parent.Id).ApplyWebConfigModifications();
                application.Update();
                log.WriteLine("All updates committed successfully");
            }
            catch (Exception e)
            {
                log.WriteLine("# An error occured when committing modifications {1}# {0}", e, Environment.NewLine);
            }

        }

        public void Dispose()
        {
            log.Flush();
            Logging(this, new EventArgs<string>(log.ToString()));
            log.Close();
        }

        private void HandleElementsWithNamespaces()
        {
            List<XElement> ToRemove = new List<XElement>();

            var NodesWithNamespaces = from attribute in FindConfigurationNodes().Attributes()
                                      where attribute.IsNamespaceDeclaration
                                      let node = attribute.Parent
                                      select new
                                      {
                                          OrginalNode = node,
                                          NamespaceUri = attribute.Value,
                                          WithoutNamespace = node.RemoveNamespace(attribute.Name.LocalName)

                                      };

            foreach (var node in NodesWithNamespaces)
            {
                log.WriteLine("Found element '{0}' which has namespace '{1}'. This needs special handling.", node.OrginalNode.Name.LocalName, node.NamespaceUri);

                var path = string.Format("{0}/*[namespace-uri()='{1}' and local-name()='{2}']",
                            BuildXPathFrom(node.OrginalNode),
                            node.NamespaceUri,
                            node.WithoutNamespace.Name.LocalName);

                log.WriteLine("Rewrote path as '{0}'.", path);

                var children = from child in node.WithoutNamespace.Elements()
                               where child.HasAttributes == false
                               select child;

                log.WriteLine("Number of childnodes to add: {0}", children.Count());

                foreach (var child in children)
                {
                    log.WriteLine("Handling childnode '{0}'", child.Name.LocalName);

                    var firstNode = child.Elements().FirstOrDefault();

                    if (firstNode == null)
                    {
                        log.WriteLine("Section '{0}' has no elements.", child.Name.LocalName);
                        continue;
                    }

                    var firstAttribute = firstNode.FirstAttribute;

                    if (firstAttribute == null)
                    {
                        log.WriteLine("Section '{0}' has no attributes.", child.Name.LocalName);
                        continue;
                    }

                    SPWebConfigModification modification = new SPWebConfigModification
                    {
                        Path = path,
                        Sequence = sequence++,
                        Owner = owner,
                        Name = string.Format("*[namespace-uri()='{0}' and local-name()='{1}']/*[namespace-uri()='{0}' and local-name()='{2}'][@{3}='{4}']/parent::*",
                                node.NamespaceUri,
                                child.Name.LocalName,
                                firstNode.Name.LocalName,
                                firstAttribute.Name.LocalName,
                                firstAttribute.Value),
                        Value = child.ToString(SaveOptions.DisableFormatting)
                    };


                    if (application.WebConfigModifications.Any(Config => Config.Equals(modification)))
                    {
                        log.WriteLine("The modification '{0}' has already been added to the collection of modifications", modification.Name);
                        continue;
                    }

                    log.WriteLine("Adding modication('{1}') '{0}' to the collection of modifications", modification.Name, modification.Type.ToString());
                    application.WebConfigModifications.Add(modification);
                }

                // remove the contents from the xml so it doesn´t get handled again
                ToRemove.Add(node.OrginalNode);
            }

            ToRemove.ForEach(node =>
            {
                log.WriteLine("Removing node '{0}' from the in-memory representation of the config file. ", node.Name.LocalName);
                node.Remove(); // remove from parent
            });

        }

        private void AddModifications(IEnumerable<XElement> elements, SPWebConfigModification.SPWebConfigModificationType modificationType)
        {

            log.WriteLine("Adding modifications of type '{0}'", modificationType.ToString());
            log.WriteLine("Found '{0}' results in feature web.config file", elements.Count());

            foreach (var item in elements)
            {

                SPWebConfigModification modification = new SPWebConfigModification
                {
                    Path = BuildXPathFrom(item),
                    Sequence = sequence++,
                    Owner = owner,
                };

                switch (modificationType)
                {
                    case SPWebConfigModification.SPWebConfigModificationType.EnsureSection:
                        modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureSection;
                        modification.Value = item.Name.LocalName;
                        modification.Name = item.Name.LocalName;
                        break;
                    case SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode:
                        modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
                        modification.Value = item.ToString(SaveOptions.DisableFormatting);
                        modification.Name = string.Format("{0}[@{1}='{2}']", item.Name.LocalName, item.FirstAttribute.Name.LocalName, item.FirstAttribute.Value);
                        break;
                    case SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute:
                        modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute;
                        modification.Value = item.FirstAttribute.Value;
                        modification.Name = item.FirstAttribute.Name.LocalName;
                        break;

                }

                if (application.WebConfigModifications.Any(Config => Config.Equals(modification)))
                {
                    log.WriteLine("The modification '{0}' has already been added to the collection of modifications", modification.Name);
                    continue;
                }

                log.WriteLine("Adding modication('{1}') '{0}' to the collection of modifications", modification.Name, modification.Type.ToString());
                application.WebConfigModifications.Add(modification);
            }
        }

        private string BuildXPathFrom(XElement node)
        {
            string from = node.Name.LocalName;
            var path = new List<string>();

            // build xpath
            while (node != null)
            {
                if (node.HasAttributes && attributemode == false)
                {
                    path.Add(string.Format("{0}[@{1}='{2}']", node.Name.LocalName, node.FirstAttribute.Name.LocalName, node.FirstAttribute.Value));
                }
                else
                {
                    path.Add(node.Name.LocalName);
                }
                node = node.Parent;
            }

            path.Reverse();

            if (attributemode == false && path.Count > 1)
            {
                path.RemoveAt(path.Count - 1);
            }

            string xpath = string.Join("/", path.ToArray());
            log.WriteLine("Building XPath from node '{0}' as '{1}'", from, xpath);

            if (from.Contains("remove") || from.Contains("clear"))
            {
                xpath = string.Concat("/", xpath);
                log.WriteLine("Changed path to '{0}'. Reason: Found 'remove' or 'clear' node that must appear first.", xpath);
            }

            return xpath;
        }

        private void ExtractWebApplicationFrom(SPFeatureReceiverProperties properties)
        {
            SPSite siteCollection = properties.Feature.Parent as SPSite;
            if (siteCollection == null)
            {
                SPWeb web = properties.Feature.Parent as SPWeb;
                if (web == null)
                {
                    log.WriteLine("# Failed to find webapplication from SPFeatureReceiverProperties. The feature will fail.");
                    throw new FileNotFoundException("Failed to find webapplication from SPFeatureReceiverProperties. The feature will fail.");
                }
                log.WriteLine("Found webapplication from site. Feature scoped as web.");
                this.application = web.Site.WebApplication;
            }
            log.WriteLine("Found webapplication from sitecollection. Feature scoped as sitecollection.");
            this.application = siteCollection.WebApplication;
        }

        private void ReadWebConfigFromFeatureDirectory(SPFeatureReceiverProperties properties)
        {
            string path = Path.Combine(properties.Definition.RootDirectory, "web.config");
            log.WriteLine("Looking for web.config file at '{0}'", path);

            webconfig = XDocument.Load(path);

            if (webconfig == null)
            {
                log.WriteLine("web.config file was not found.");
                throw new FileNotFoundException("web.config file was not found");
            }

            log.WriteLine("web.config file found");
        }

        private IEnumerable<XElement> FindConfigurationNodes()
        {
            return webconfig.Root.Descendants();
        } 

        #endregion

    }
}
